// TODO: CODE REVIEW & CLEANUP!
//
// LutronSimulator.cs
//
// Implements HomeUX.Components.LutronSimulator.
//

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO.Ports;
using System.Linq;
using System.Text;
using HomeUX.Utilities;

namespace HomeUX.Components
{

/// <summary>
/// Simulates the Lutron system using data from LutronExport\LutronData.cs
/// to determine the configuration of the Lutron system.
/// </summary>
/// <remarks>
/// This is a limited simulation.  Specifically:
/// <list type="bullet">
/// <item>Only these commands are currently supported: "KBP" (keypad
/// 	button press), "RKLS" (read keypad LED state), "FRPM" (fade remote
/// 	power module), "FV" (fade Vareo), "RDL" (request a dimmer level).
/// 	</item>
/// <item>Only these notifications are currently sent: "KBP" (keypad button
/// 	press), "KLS" (keypad LED state), "DL" (dimmer level state).
/// 	</item>
/// <item>The "FRPM" and "FV" commands ignore the delay time parameter.
/// 	</item>
/// <item>The only input address format supported is dot-delimited.  For
/// 	example, "kbp,1.2.3,4" is accepted, but "kbp,[01:02:03],4" is not.
/// 	However, the bracketed format (e.g. "[01:02:03]") format is output
/// 	within notifications, for compatibility with Lutron output.</item>
/// <item>LED blinking is not supported.</item>
/// <item>All lighting assumes "scene" mode: an LED is on (or off, if
///     it has the "Reverse LED" setting) if loads are at the levels
///     specified by that button's assignments.  "Room" mode (LED on if
///     any load is at the level specified by that button's
///     assignments) is not supported.</item>
/// <item>Using the sample Lutron data, the following keypad buttons don't
/// 	behave as you'd expect: "1.4.5,7" (Entry Wall: To Game Room),
/// 	"1.4.20,11" (Wall By Bedroom: To Kitchen), "1.4.20,12" (Wall By
/// 	Bedroom: To Game Room).  These have prgm_type == 5 in the Lutron
/// 	database; I forget what that means, but this simulator doesn't
/// 	support it.</item>
/// </list>
/// </remarks>
///
public class LutronSimulator : VirtualSerialPort
{
	//////////////////////////////////////////////////////////////////////
	// Additional Simulator Constants (in addition to LutronData.cs)
	//

	/// <summary>
	/// The address (in the form of "<i>processor</i>.<i>link</i>" of the
	/// keypad links.
	/// </summary>
	readonly static string[] KeypadLinks = new string[] { "1.4", "1.6" };

	//////////////////////////////////////////////////////////////////////
	// Private State
	//

	/// <summary>
	/// This object is locked before a thread accesses this object, to
	/// serialize access to the object.
	/// </summary>
	LutronSimulatorLock m_lock = new LutronSimulatorLock();

	/// <summary>
	/// Serial port read buffer, i.e. data to send to client.
	/// </summary>
	StringBuilder m_sendToClient = new StringBuilder(1000);

    /// <summary>
    /// Work queue for scheduling asynchronous operations.
    /// </summary>
    WorkQueue m_workQueue;

    /// <summary>
    /// Identifies a test being performed using this simulator, or 0 if no test is being
    /// performed.
    /// </summary>
    int _test;

	/// <summary>
	/// Maps a Lutron keypad address, e.g. "1.2.3", to a
	/// <r>LutronKeypadState</r> object that contains information about a
	/// Lutron keypad or a device (e.g. a CCO/CCI) that behaves like
	/// a keypad.  Each <r>LutronKeypadState</r> object includes static
	/// information about that keypad, as well as its current simulated
	/// state (i.e. which LEDs are on and whether any action sequences are
	/// currently executing).
	/// </summary>
	Dictionary<string, LutronKeypadState> m_addressToKeypad;

	/// <summary>
	/// Maps a Lutron load address, e.g. "1.2.3.4.5", to a
	/// <r>LutronLoadState</r> object that contains information about a
	/// Lutron load, i.e. an RPM (remote power modules), Vareo (remotely
	/// controllable wall switch/dimmers), or CCO (relay).  Each
	/// <r>LutronLoadState</r> object includes static information about
	/// that load, as well as its current simulated state (the level of
	/// the load 0% to 100%).
	/// </summary>
	Dictionary<string, LutronLoadState> m_addressToLoad;

	/// <summary>
	/// Indicates that one or more LEDs in the system changed since the
	/// last call to <r>ResetPendingLedNotifications</r>.
	/// <r>SendLedNotifications</r> uses this information to decide if
	/// "KLS" notifications need to be sent to the client.
	/// </summary>
	bool LedsChanged;

    //////////////////////////////////////////////////////////////////////
    // IDisposable Overrides
    //

    public override void Dispose()
    {
        base.Dispose();
        if (m_workQueue != null)
        {
            m_workQueue.Dispose();
            m_workQueue = null;
        }
    }

	//////////////////////////////////////////////////////////////////////
	// Public Methods
	//

	/// <summary>
	/// Creates an instance of this class.
	/// </summary>
	///
	public LutronSimulator() : this(new string[0])
	{
	}

    public LutronSimulator(string[] args)
    {
        // parse <args>
        foreach (string arg in args)
        {
            try
            {
                string[] argParts = arg.Split('=');
                if (argParts.Length != 2)
                    throw new ArgumentException();
                string argName = argParts[0];
                string argValue = argParts[1];
                switch (argName)
                {
                case "Test":
                    _test = System.Xml.XmlConvert.ToInt32(argValue);
                    break;
                default:
                    throw new ArgumentException();
                }
            }
            catch (Exception ex)
            {
                if ((ex is FormatException) || (ex is OverflowException) ||
                    (ex is ArgumentException))
                {
                    throw new VirtualSerialPortException("Invalid argument: {0}", arg);
                }
                else
                    throw;
            }
        }
    }

	//////////////////////////////////////////////////////////////////////
	// VirtualSerialPort Overrides
	//

	public override bool SupportsLineBasedTextMode
	{
		// this virtual serial port only supports line-based text mode
		get
		{
			return true;
		}
	}

	public override string NewLine
	{
        // Lutron uses carriage return as the newline character
		get
		{
            return "\r";
		}
        set
        {
            if (value != "\r")
            {
                throw new InvalidOperationException(
					"Lutron uses carriage return as the newline sequence");
			}
        }
	}

    public override void Open()
    {
        lock (m_lock)
        {
            // initialize <m_addressToKeypad>
            m_addressToKeypad = new Dictionary<string, LutronKeypadState>(
                LutronData.Keypads.Length);
            foreach (int iKeypad in Util.IndicesOf(LutronData.Keypads))
            {
                LutronKeypadState keypad = new LutronKeypadState(this,
                    LutronData.Keypads[iKeypad]);
                m_addressToKeypad.Add(keypad.Address, keypad);
            }

            // initialize <m_addressToLoad>
            m_addressToLoad = new Dictionary<string, LutronLoadState>(
                LutronData.Loads.Length);
            foreach (int iLoad in Util.IndicesOf(LutronData.Loads))
            {
                LutronLoadState load =
                    new LutronLoadState(this, LutronData.Loads[iLoad]);
                m_addressToLoad.Add(load.Address, load);
            }

            // initialize each LutronLoadState.LedContributions and each
            // LutronButtonState.FullLedPoints -- see comments for those
            // fields for more information about the LED "points" algorithm
            foreach (LutronButtonState button in Buttons)
            {
                // the LED "points" algorithm is not used for sequenced
                // buttons -- for those, the LED is on while sequencing is
                // in progress
                if (button.IsSequenced)
                    continue;

                // calculate the LED "points" for the LED of this button
                button.FullLedPoints = 0;
                foreach (LutronAssignment assignment in button.Assignments)
                {
                    int level =
                        ParseLutronLevel(assignment.AssignValue, true);
                    if (level == -1) // -1 means "Unaffected"
                        continue;
                    LutronLoadState load =
                        AddressToLoad(assignment.LoadAddress);
                    LedContribution contribution = new LedContribution();
                    contribution.Level =
                        ParseLutronLevel(assignment.AssignValue, true);
                    contribution.Button = button;
                    load.LedContributions.Add(contribution);
                    button.FullLedPoints++;
                }
            }

            // initialize the state of all LEDs to correspond with the
            // current state of all loads
            UpdateLedsBasedOnLoads();

            // create a work queue for queueing events
            m_workQueue = new WorkQueue("LutronSimulator");

            // wait, then write "AT&F1E0F1Q1S0=1M0&C0&D0", then wait, then
            // write it again
            When now = m_workQueue.Now;
            m_workQueue.Add(now + 500, delegate
            {
                WriteToClient("\n\n\n\n\nAT&F1E0F1Q1S0=1M0&C0&D0\n");
            });
            m_workQueue.Add(now + 1500, delegate
            {
                WriteToClient("\n\n\n\n\nAT&F1E0F1Q1S0=1M0&C0&D0\n");
            });

            // debugging feedback
            Trace("Initialized");
        }
    }

	public override string ReadExisting()
	{
        lock (m_lock)
		{
			string text = m_sendToClient.ToString();
			m_sendToClient.Length = 0;
			return text;
		}
	}

	public override void WriteLine(string text)
	{
		// client sent us a command (<text>)
		lock (m_lock)
		{
			try
			{
				Trace("Received command: {0}", text);
				ExecuteLutronCommand(Util.SplitAndTrim(text, ','));
			}
			catch (LutronInvalidCommandException)
			{
				WriteToClient("\n\nError: Invalid Command\n");
			}
			WriteToClient("\nL232> ");
		}
	}

	//////////////////////////////////////////////////////////////////////
	// Private Nested Classes
	//

	/// <summary>
	/// Indicates an invalid Lutron command.
	/// </summary>
	///
	class LutronInvalidCommandException : Exception
	{
	}

	//////////////////////////////////////////////////////////////////////
	// Private Properties & Methods
	//
	// NOTE: These private properties and methods are generally NOT
	// thread-safe -- call these methods within lock(m_lock) or an
	// equivalent.
	// 

    /// <summary>
	/// Gets an enumeration of all keypads in the Lutron system.
	/// </summary>
	///
	IEnumerable<LutronKeypadState> Keypads
	{
		get
		{
			foreach (KeyValuePair<string, LutronKeypadState> kvp in
				m_addressToKeypad)
			{
				yield return kvp.Value;
			}
		}
	}

	/// <summary>
	/// Gets an enumeration of all buttons in the Lutron system.
	/// </summary>
	///
	IEnumerable<LutronButtonState> Buttons
	{
		get
		{
			foreach (KeyValuePair<string, LutronKeypadState> kvp in
				m_addressToKeypad)
			{
				LutronKeypadState keypad = kvp.Value;
				foreach (int buttonId in keypad.ButtonPositions)
				{
					LutronButtonState button = keypad.Buttons[buttonId];
					if ((button != null) && button.Exists)
						yield return button;
				}
			}
		}
	}

	/// <summary>
	/// Gets an enumeration of all loads in the Lutron system.
	/// </summary>
	///
	IEnumerable<LutronLoadState> Loads
	{
		get
		{
			foreach (KeyValuePair<string, LutronLoadState> kvp in
				m_addressToLoad)
			{
				yield return kvp.Value;
			}
		}
	}

	/// <summary>
	/// Executes a Lutron command sent from the client.
	/// </summary>
	///
	/// <param name="command">The Lutron command to execute.  Each element
	/// 	of the array is one part of the command that was
	/// 	comma-delimited in the original string command.  Elements of
	/// 	the array are trimmed of leading and trailing spaces.  For
	/// 	example, the command <n>"kbp, 1.6.2, 12"</n> is passed in as
	/// 	three elements: <n>"kbp"</n>, <n>"1.6.2"</n>, and <n>"12"</n>.
	/// 	</param>
	///
	void ExecuteLutronCommand(string[] command)
	{
		/*
		Example:
		  [Lutron] <-- KBP,1.6.2,12	// phantom button "E L-Shade M Down"
		  [Lutron] --- L232> 
		  [Lutron] --- KBP, [01:06:02], 12
		  [Lutron] --- KLS, [01:06:02], 000000000001000000000000
		  [Lutron] --> KLS to <All>: 1.6.2,12=1
		  [Lutron] --- KLS, [01:06:18], 000000000111111120000000
		  [Lutron] --- KLS, [01:06:18], 000000000111111110000000
		  [Lutron] --- KLS, [01:06:02], 000000000000000000000000
		  [Lutron] --> KLS to <All>: 1.6.2,12=0

		Commands:
		  -- KBP <processor>.<link>.<address>, <button>

		Ranges:
		  -- <processor> = 1 to 16
		  -- <link> = 4 to 6
		  -- <address> = 1 to 32

		Errors:
		  L232> ab


		  Error: Invalid Command

		  L232>


		Notes:
		  -- Lutron doesn't send an "L232>" after initial "AT&F...".
		  -- Lutron sends back "\n\nError: Invalid Command\n" on any kind
			 of error, including syntax error, nonexistent devices, etc.
		  -- Notifications have a blank line between each one.
		  -- I've never seen the text "L232>" or "Error: Invalid Command"
			 be split, e.g. "L23KBP, [01:06:02], 122>".  But, the client
			 sends a command, it may get one or more lines of notification
			 (each beginning on a new line) before it gets either "L232>"
			 (beginning on a new line), indicating success, or "Error:
			 Invalid Command" (beginning on a new line), indicating failure.
		  -- I think I can ignore fade times in LutronSimulator.  For example,
			 when you press a "House Off" button, which fades many lights
			 over 3 seconds, Lutron seems to send notifications immediately,
			 as if new levels take effect right away and only the physical
			 equipment slowly fades to the new level.
		*/

		// set <commandName> to the command name, converted to uppercase;
		// error if there is no command name
		if (command.Length == 0)
			throw new LutronInvalidCommandException();
		string commandName = command[0].ToUpper();

		// interpret the command
		string keypadAddress, loadAddress;
		LutronKeypadState keypad;
		LutronLoadState load;
		switch (commandName)
		{

		case "KBP": // keypad button press

			// format: "KBP, <address>, <button>"
			// e.g. "KBP,1.2.3,4"
			if (command.Length != 3)
				throw new LutronInvalidCommandException();
			keypadAddress = ParseAddress(command[1]);
			int buttonId = ParseButtonNumber(command[2]);
			keypad = AddressToKeypad(keypadAddress);
            if (keypad.ButtonCount < 0)
                break;
			LutronButtonState button = keypad.NumberToButton(buttonId);
			WriteToClient("\nKBP, {0}, {1}\n",
				AddressToSquareBrackets(keypad.Address), button.ButtonId);
			ExecuteButtonPressCommand(button);
			break;

		case "RKLS": // read keypad LED state

			// format: "RKLS, <address>"
			// e.g. "RKLS,1.2.3"
			if (command.Length != 2)
				throw new LutronInvalidCommandException();
			keypadAddress = ParseAddress(command[1]);
			keypad = AddressToKeypad(keypadAddress);
            if (keypad.ButtonCount < 0)
                break;
            if (_test != 1)
            {
                WriteToClient("\nKLS, {0}, {1}\n",
                    AddressToSquareBrackets(keypad.Address),
                    keypad.LedStatesString);
            }
			break;

		case "FRPM": // fade remote power module
		case "FV": // fade Vareo

			// format: "FRPM, <level>, <fade time>, <delay time>,
			//                <address 1>,...,<address 10>"
			// e.g. "FRPM, 100, 00:02, 1.2.3.4.5, 1.2.3.4.6"
			// (note that LutronSimulator doesn't distinguish between an RPM
			// and a Vareo)
			if (command.Length < 5)
				throw new LutronInvalidCommandException();
			int newLevel = ParseLutronLevel(command[1], false);
			long fadeTime = ParseLutronTime(command[2]);
			long delayTime = ParseLutronTime(command[3]);
			int loadCount = command.Length - 4;
			LutronLoadState[] loads = new LutronLoadState[loadCount];
			foreach (int iLoad in Util.IndicesOf(loads))
				loads[iLoad] = AddressToLoad(command[iLoad + 4]);
			ExecuteFadeRpmCommand(loads, newLevel);
			break;

		case "RDL": // request a dimmer level

			// format: "RDL, <address>"
			// e.g. "RDL,1.2.3.4.5"
			if (command.Length != 2)
				throw new LutronInvalidCommandException();
			loadAddress = ParseAddress(command[1]);
			load = AddressToLoad(loadAddress);
			WriteToClient("\nDL, {0}, {1}\n",
				AddressToSquareBrackets(load.Address), load.Level);
			break;

		default:

			throw new LutronInvalidCommandException();

		}
	}

	/// <summary>
	/// Executes a Lutron "KBP" command sent from the client.  This command
	/// tells the Lutron system to simulate a keypad button press.
	/// </summary>
	///
	/// <param name="button">The button being "virtually" pressed.
	/// 	</param>
	///
	public void ExecuteButtonPressCommand(LutronButtonState button)
	{
		if (button.IsSequenced)
		{
			// this is a sequenced set of actions, e.g. "shades up"
			// (turn on motors, wait 15 seconds, turn off motors)...

			// if a sequence is currently executing for <button>,
			// execute the termination step and turn off the button's LED
			LutronSequence sequence;
			if (button.ExecutingSequence != null)
			{
				// remove the work item corresponding to
				// <button.ExecutingSequence> from the work queue; if, on
				// another thread, this item was removed from the queue
				// before LockDuring() was called above, just quit (nothing
				// further to do since the caller's intent was already
				// realized)
				sequence = button.ExecutingSequence;
                m_workQueue.AbortWorkItemsWithKey(sequence);

				// execute the termination step (if any)
				if (button.HasTerminateAction)
				{
					sequence.NextStepIndex = sequence.Steps.Count - 1;
					sequence.ExecuteNextStep();
				}

				// turn off the button's LED
				SetLedState(button, false);

				// no sequence executing on <button> now
				button.ExecutingSequence = null;

				// send "KLS" notifications (if any) to the client, caused
				// by changes to LED states that occurred above
				UpdateLedsBasedOnLoads();
				SendLedNotifications();

				// done this "KBP" command
				return;
			}

			// set <sequence> to a new LutronSequence object that collects
			// the LutronAssignment objects within <button.Actions> into
			// steps, where each step contains all the assignments executed
			// at the same time
			sequence = SequenceFromButton(button);
			sequence.NextStepTime = sequence.StartTime = m_workQueue.Now;

			// record the fact that this button is executing <sequence>
			button.ExecutingSequence = sequence;

			// turn on the button's LED during sequencing
			SetLedState(button, true);

			// execute the first step of <sequence>
			sequence.ExecuteNextStep();

			if (sequence.AtEnd)
			{
				// there was only one step in the sequence, so we're done
				SetLedState(button, false);

				// no sequence executing on <button> now
				button.ExecutingSequence = null;
			}
			else
			{
				// queue the next step of <sequence>
                var workItem = new WorkItemWithKey<object>(sequence.NextStepTime, sequence,
                    delegate
                    {
                        sequence.TimerCallback(sequence);
                    });
				m_workQueue.Add(workItem);
			}

			// send "KLS" notifications (if any) to the client, caused by
			// changes to LED states that occurred above
			UpdateLedsBasedOnLoads();
			SendLedNotifications();
			return;
		}

		// this is a non-sequenced button, e.g it's a scene on/off/toggle...
		if (button.LedOn != button.LedReverse/*xxx*/)
		{
			// the scene is already fully "on"
			if (!button.Toggle)
			{
				// this button doesn't toggle on/off, so we're done
				return;
			}

			// toggle the scene
			foreach (LutronAssignment assignment in button.Assignments)
				ToggleAssignment(assignment);
		}
		else
		{
			// this scene isn't already fully "on" -- apply each
			// LutronAssignment associated with this button, i.e. set loads
			// to new levels as defined by those assignments; ignore fade
			// times (LutronAssignment.AssignFade) because Lutron seems to
			// send notifications immediately and only the physical
			// equipment slowly fades to the new level
			foreach (LutronAssignment assignment in button.Assignments)
				ExecuteAssignment(assignment);
		}

		// send "KLS" notifications (if any) to the client, caused by
		// changes to LED states that occurred above
		UpdateLedsBasedOnLoads();
		SendLedNotifications();
	}

	/// <summary>
	/// Executes a given <r>LutronAssignment</r>, i.e. sets the load
	/// specified in the assignment to a given level.
	/// </summary>
	///
	/// <param name="assignment">The assignment to execute.</param>
	///
	void ExecuteAssignment(LutronAssignment assignment)
	{
		LutronLoadState load = AddressToLoad(assignment.LoadAddress);
		int newLevel = ParseLutronLevel(assignment.AssignValue, true);
		SetLoadLevel(load, newLevel);
	}

	/// <summary>
	/// Toggles a given <r>LutronAssignment</r>, i.e. sets the load
	/// specified in the assignment to the load's default level.
	/// </summary>
	///
	/// <param name="assignment">The assignment to toggle.</param>
	///
	void ToggleAssignment(LutronAssignment assignment)
	{
		LutronLoadState load = AddressToLoad(assignment.LoadAddress);
		SetLoadLevel(load, 0);
	}

	/// <summary>
	/// Constructs a <r>LutronSequence</r> from the set of actions
	/// associated with a Lutron keypad button.
	/// </summary>
	///
	/// <param name="button">The button to create a sequence from.</param>
	///
	/// <remarks>
	/// The <r>LutronSequenceStep</r> class groups all a button's
	/// associated <r>LutronAssignment</r> objects that are supposed to
	/// happen at the same time.  The <r>LutronSequence</r> returned by
	/// this method is an ordered sequence of <r>LutronSequenceStep</r>
	/// objects.  Note that <s>LutronSequence.StartTime</s> is zero in the returned
	/// <r>LutronSequence</r>.
	/// </remarks>
	///
	LutronSequence SequenceFromButton(LutronButtonState button)
	{
		// set <sequence> to a new LutronSequence, start initializing it
		LutronSequence sequence = new LutronSequence();
		sequence.Owner = this;
		sequence.Button = button;
		sequence.Steps = new List<LutronSequenceStep>();
		sequence.NextStepIndex = 0;

		// if there are no actions in <button>, return an empty
		// LutronSequenceStep array
		if (button.Actions.Length == 0)
			return sequence;

		// enumerate through the actions of this button, and collect
		// assignments into <sequence.Steps>
		int iActionBeginStep = 0;
		int iAction = 0;
		while (true)
		{
			iAction++;
			bool lastAction = (iAction == button.Actions.Length - 1);
			bool endOfActions = (iAction >= button.Actions.Length);
			long actionDwell;
			bool endOfStep = endOfActions ||
				(lastAction && button.HasTerminateAction) ||
				((actionDwell =
				  ParseLutronTime(button.Actions[iAction - 1].Dwell)) != 0);
			if (endOfStep)
			{
				LutronSequenceStep step = SequenceStepFromActions(
					button.Actions, iActionBeginStep,
					iAction - iActionBeginStep);
				sequence.Steps.Add(step);
				iActionBeginStep = iAction;
			}
			if (endOfActions)
				break;
		}

		return sequence;
	}

	/// <summary>
	/// Collects the <r>LutronAssignment</r> objects inside a subarray of
	/// <r>LutronAction</r> objects into a <r>LutronSequenceStep</r>
	/// object.
	/// </summary>
	///
	/// <param name="actions">The <r>LutronAction</r> array from which the
	/// 	subarray is drawn.</param>
	///
	/// <param name="iActionBeginStep">The index within
	/// 	<pr>actions</pr> of the first <r>LutronAction</r>
	/// 	in the subarray.</param>
	///
	/// <param name="cActions">The number of elements in the subarray
	/// 	of <r>LutronAction</r> objects in <pr>actions</pr>.
	/// 	</param>
	///
	LutronSequenceStep SequenceStepFromActions(LutronAction[] actions,
		int iActionBeginStep, int cActions)
	{
		// set <cAssignments> to the total number of assignments in the
		// specified range of actions
		int cAssignments = 0;
		for (int iAction = iActionBeginStep;
			 iAction < iActionBeginStep + cActions;
			 iAction++)
		{
			cAssignments += actions[iAction].Assignments.Length;
		}

		// collect the assignments into a new LutronSequenceStep object
		LutronSequenceStep step = new LutronSequenceStep();
		step.Assignments = new LutronAssignment[cAssignments];
		if (cAssignments > 0)
		{
			int iAssignment = 0;
			LutronAction lastAction = null;
			for (int iAction = iActionBeginStep;
				 iAction < iActionBeginStep + cActions;
				 iAction++)
			{
				lastAction = actions[iAction];
				foreach (LutronAssignment assignment in
						lastAction.Assignments)
					step.Assignments[iAssignment++] = assignment;
			}
			step.DwellTime = ParseLutronTime(lastAction.Dwell);
		}

		return step;
	}

	/// <summary>
	/// Executes a Lutron "FRPM" command sent from the client.  This
	/// command tells the Lutron system to fade a remote power module to a
	/// given level.
	/// </summary>
	///
	/// <param name="loads">The loads to set to a new level.</param>
	///
	/// <param name="newLevel">The new level (a percent value from 0 to
	/// 	100) to set the loads to.</param>
	///
	/// <remarks>
	/// The <r>LutronSimulator</r> implementation ignores the fade and delay
	/// parameters.
	/// </remarks>
	///
	void ExecuteFadeRpmCommand(LutronLoadState[] loads, int newLevel)
	{
		// update the load levels
		foreach (LutronLoadState load in loads)
			SetLoadLevel(load, newLevel);

		// send "KLS" notifications (if any) to the client, caused by
		// changes to LED states that occurred above
		UpdateLedsBasedOnLoads();
		SendLedNotifications();
	}

	/// <summary>
	/// Sets a Lutron load to a new level, and sends the corresponding
	/// notification to the client (if the level changed).
	/// </summary>
	///
	/// <param name="load">The load to set to a new level.</param>
	///
	/// <param name="newLevel">The new level to set the load to.  This is
	/// 	a percent value from 0 to 100, or -1 which means "Unaffected";
	/// 	in the latter case the load level is not changed.</param>
	///
	void SetLoadLevel(LutronLoadState load, int newLevel)
	{
		if ((newLevel != -1/*Unaffected*/) && (load.Level != newLevel))
		{
			load.Level = newLevel;
			Trace("Load changed: {0} --> {1}%", load, load.Level);
            if (!IsKeypadLinkAddress(load.Address))
            {
                WriteToClient("\nDL, {0}, {1}\n",
                    AddressToSquareBrackets(load.Address), load.Level);
            }
		}
	}

    /// <summary>
    /// Returns <n>true</n> if a given address (e.g "1.2.3") is the address of
    /// a Lutron hardware element that's on a keypad link (as opposed to a
    /// RPM or Vareo link).
    /// </summary>
    ///
    /// <param name="address">The address to check.</param>
    ///
    bool IsKeypadLinkAddress(string address)
    {
        string[] parts = address.Split('.');
        if (parts.Length < 2)
            return false;
        string addressLink = String.Format("{0}.{1}", parts[0], parts[1]);
        return KeypadLinks.Contains(addressLink);
    }

	/// <summary>
	/// Sets the LED of a keypad button to be on or off.  Also, 
	/// <r>LutronKeypadState.LedsChanged</r> is set to true for the keypad,
	/// and <r>LutronSimulator.LedsChanged</r> is also set to true, so that
	/// a subsequent call to <r>SendLedNotifications</r> will notify the
	/// client that the LED state has changed.
	/// </summary>
	///
	/// <param name="button">The button to adjust the LED of.</param>
	///
	/// <param name="ledOn">true to turn the LED on, false to turn it
	/// 	off.</param>
	///
	/// <remarks>
	/// This method does not send the client a notification that the
	/// LED has changed -- the caller must call <r>SendLedNotifications</r>
	/// to do that, so that all notifications resulting from a given
	/// client command (for example) are batched together.
	/// </remarks>
	///
	void SetLedState(LutronButtonState button, bool ledOn)
	{
		if (button.LedOn != ledOn)
		{
			button.LedOn = ledOn;
			button.Keypad.LedsChanged = true;
			LedsChanged = true;
		}
	}

	/// <summary>
	/// Sets <r>LutronKeypadState.LedsChanged</r> to false for all keypads,
	/// and also sets <r>LutronSimulator.LedsChanged</r> to false, indicating
	/// that no LEDs have changed yet.  Subsequent calls to
	/// <r>UpdateLedsBasedOnLoads</r> may set these flags.
	/// </summary>
	///
	void ResetPendingLedNotifications()
	{
		foreach (LutronKeypadState keypad in Keypads)
			keypad.LedsChanged = false;
		LedsChanged = false;
	}

	/// <summary>
	/// Updates the state of all LEDs (<r>LutronButtonState.LedOn</r>)
	/// based on the state of all loads.  Also, for any keypad with an LED
	/// that changed, <r>LutronKeypadState.LedsChanged</r> is set to true,
	/// and if any LED in the entire system has changed
	/// <r>LutronSimulator.LedsChanged</r> is set to true, so that a subsequent
	/// call to <r>SendLedNotifications</r> will notify the client that LED
	/// states have changed.
	/// </summary>
	///
	/// <remarks>
	/// <para>
	/// This method does not send the client a notification that the
	/// LED has changed -- the caller must call <r>SendLedNotifications</r>
	/// to do that, so that all notifications resulting from a given
	/// client command (for example) are batched together.
	/// </para>
	/// <para>
	/// This method is somewhat expensive, so it should only be called
	/// once per command.
	/// </para>
	/// </remarks>
	///
	void UpdateLedsBasedOnLoads()
	{
		// reset LutronButtonState.LedPoints to 0 for each button
		foreach(LutronButtonState button in Buttons)
		{
			button.LedPoints = 0;
		}

		// update LutronButtonState.LedPoints based on the state of each
		// load
		foreach (LutronLoadState load in Loads)
		{
			foreach (LedContribution contribution in load.LedContributions)
			{
				if (contribution.Level == load.Level)
					contribution.Button.LedPoints++;
			}
		}

		// for each button, see if its LED state needs to change based
		// on LutronButtonState.LedPoints
		foreach(LutronButtonState button in Buttons)
		{
			// ignore sequenced buttons
			if (button.IsSequenced)
				continue;

			// set <on> to true iff the LED should be on
			bool on = (button.LedPoints == button.FullLedPoints);
			if (button.LedReverse)
				on = !on;

			// change the LED state (and notify clients of the change, if
			// requested) if the LED state needs changing
			if (button.LedOn != on)
			{
				// update the LED state
				button.LedOn = on;

				// update global state
				button.Keypad.LedsChanged = true;
				LedsChanged = true;

				// provide debugging information
				Trace("LED changed: {0} {1} --> {2}", button.Keypad, button,
					OnOff(on));
			}
		}
	}

	/// <summary>
	/// Sends "KLS" notifications to the client about any LEDs whose
	/// <r>LutronButtonState.LedOn</r> state has changed (via calls to
	/// <r>UpdateLedsBasedOnLoads</r>) since the last call to
	/// <r>ResetPendingLedNotifications</r>.  Then,
	/// <r>ResetPendingLedNotifications</r> is called.
	/// </summary>
	///
	void SendLedNotifications()
	{
		if (LedsChanged)
		{
			foreach (LutronKeypadState keypad in Keypads)
			{
				if (keypad.LedsChanged)
				{
					WriteToClient("\nKLS, {0}, {1}\n",
						AddressToSquareBrackets(keypad.Address),
						keypad.LedStatesString);
				}
			}
			ResetPendingLedNotifications();
		}
	}

	/// <summary>
	/// Normalizes a Lutron keypad address like "1. 4 .5 " (note the
	/// spaces) to "1.4.5".
	/// </summary>
	///
	/// <remarks>
	/// On error, a <r>LutronInvalidCommandException</r> is thrown.
	/// </remarks>
	///
	static string ParseAddress(string address)
	{
		return address.Replace(" ", "");
	}

	/// <summary>
	/// Finds the <r>LutronKeypadState</r> object corresponding to a
	/// normalized Lutron keypad address, e.g. "1.2.3".
	/// </summary>
	///
	/// <param name="keypadAddress">A normalized Lutron keypad address,
	/// 	e.g. "1.2.3".  Use <r>ParseAddress</r> to normalize the
	/// 	address before calling this method.</param>
	///
	/// <remarks>
	/// On error, a <r>LutronInvalidCommandException</r> is thrown.
	/// </remarks>
	///
	LutronKeypadState AddressToKeypad(string keypadAddress)
	{
		try
		{
			return m_addressToKeypad[keypadAddress];
		}
		catch (KeyNotFoundException)
		{
			throw new LutronInvalidCommandException();
		}
	}

	/// <summary>
	/// Parses a Lutron button number.
	/// </summary>
	///
	/// <remarks>
	/// On error, a <r>LutronInvalidCommandException</r> is thrown.
	/// </remarks>
	///
	static int ParseButtonNumber(string button)
	{
		try
		{
			return int.Parse(button);
		}
		catch (FormatException)
		{
			throw new LutronInvalidCommandException();
		}
	}

	/// <summary>
	/// Finds the <r>LutronLoadState</r> object corresponding to a
	/// normalized Lutron load address, e.g. "1.2.3.4".
	/// </summary>
	///
	/// <param name="loadAddress">A normalized Lutron load address, e.g.
	/// "1.2.3.4".</param>
	///
	/// <remarks>
	/// On error, a <r>LutronInvalidCommandException</r> is thrown.
	/// </remarks>
	///
	LutronLoadState AddressToLoad(string loadAddress)
	{
		try
		{
			return m_addressToLoad[loadAddress];
		}
		catch (KeyNotFoundException)
		{
			throw new LutronInvalidCommandException();
		}
	}

	/// <summary>
	/// Parses a Lutron time, e.g. "00:00" (zero time) or "00:19.50"
	/// (19.5 seconds), and returns a corresponding number of milliseconds.
	/// </summary>
	///
	/// <remarks>
	/// On error, a <r>LutronInvalidCommandException</r> is thrown.
	/// </remarks>
	///
	static long ParseLutronTime(string lutronTime)
	{
		// set <minutes> to the minutes part of <lutronTime>; set
		// <secondsParts> to the seconds part
		int minutes;
		string secondsPart;
		string[] parts = lutronTime.Split(':');
		if (parts.Length == 1)
		{
			minutes = 0;
			secondsPart = parts[0];
		}
		else
		if (parts.Length == 2)
		{
			try
			{
				minutes = int.Parse(parts[0]);
			}
			catch (FormatException)
			{
				throw new LutronInvalidCommandException();
			}
			secondsPart = parts[1];
		}
		else
			throw new LutronSimulator.LutronInvalidCommandException();

		// calculate the specified time
		double seconds;
		try
		{
			seconds = double.Parse(secondsPart,
				System.Globalization.CultureInfo.InvariantCulture);
		}
		catch (FormatException)
		{
			throw new LutronInvalidCommandException();
		}
		return ((long) minutes * 60 * 1000) +
			(long) Math.Round(seconds * 1000);
	}

	/// <summary>
	/// Parses a Lutron load level, e.g. "ON", "OFF", "0%", "50%", "100%",
	/// "0", "50", "100", "Unaffected", etc., and returns a corresponding
	/// percent value (0 to 100), or -1 for "" or "Unaffected".
	/// </summary>
	///
	/// <param name="unaffectedOk">True if the "Unaffected" or "" is
	///		allowed, false if these are treated as errors.  If
	/// 	<pr>unaffectedOk</pr> is false, -1 will not be
	/// 	returned by this method.</param>
	///
	/// <remarks>
	/// On error, a <r>LutronInvalidCommandException</r> is thrown.
	/// </remarks>
	///
	int ParseLutronLevel(string lutronLevel, bool unaffectedOk)
	{
		if ((lutronLevel.Length == 0) ||
			(string.Compare(lutronLevel, "Unaffected", StringComparison.InvariantCultureIgnoreCase) == 0))
		{
			if (unaffectedOk)
				return -1;
			else
				throw new LutronInvalidCommandException();
		}
		else
		if (string.Compare(lutronLevel, "ON", StringComparison.InvariantCultureIgnoreCase) == 0)
			return 100;
		else
		if (string.Compare(lutronLevel, "OFF", StringComparison.InvariantCultureIgnoreCase) == 0)
			return 0;
		else
		{
			if (lutronLevel.EndsWith("%"))
				lutronLevel = lutronLevel.Substring(0,
					lutronLevel.Length - 1);
			try
			{
				return int.Parse(lutronLevel);
			}
			catch (FormatException)
			{
				throw new LutronInvalidCommandException();
			}
		}
	}

	/// <summary>
	/// Formats a string and sends it string to the client asynchronously.
	/// Line feed characters in the string are converted to
	/// <r>VirtualSerialPort.NewLine</r>.  The client will receive the string on the
	/// virtual serial port.
	/// </summary>
	///
	void WriteToClient(string format, params object[] args)
	{
		WriteToClient(String.Format(format, args));
	}

	/// <summary>
	/// Sends a string to the client asynchronously.  Line feed characters
	/// in the string are converted to <r>VirtualSerialPort.NewLine</r>.  The client will
	/// receive the string on the virtual serial port.
	/// </summary>
	///
	void WriteToClient(string text)
	{
		lock (m_lock)
			m_sendToClient.Append(text.Replace("\n", NewLine));
		m_workQueue.Add(delegate
		{
            FireDataReceived(SerialData.Chars);
		});
	}

	/// <summary>
	/// Converts bool values true and false to strings "ON" and "OFF",
	/// respectively.
	/// </summary>
	///
	static string OnOff(bool on)
	{
		return (on ? "ON" : "OFF");
	}

	/// <summary>
	/// Converts a dotted address, e.g. "1.7.12", to a Lutron-style
	/// square bracket address, e.g. "[01:07:12]".
	/// </summary>
	///
	static string AddressToSquareBrackets(string address)
	{
		StringBuilder result = new StringBuilder("[", 50);
		foreach (string part in address.Split('.'))
		{
			if (result.Length > 1)
				result.Append(":");
			if (part.Length < 2)
				result.Append("0");
			result.Append(part);
		}
		result.Append("]");
		return result.ToString();
	}

	/// <summary>
	/// Writes a formatted string to the debug output window.
	/// </summary>
	///
	static void Trace(string format, params object[] args)
	{
#if !SILVERLIGHT
		Util.Trace("[LutronSimulator] {0}", String.Format(format, args));
#endif
	}

	/// <summary>
	/// Represents a Lutron keypad, or a device (e.g. a CCO/CCI) that
	/// behaves like a keypad.  Includes static information about the
	/// keypad, as well as its current simulated state.
	/// </summary>
	///
	[DebuggerDisplay("{Address}: {Name}: LEDs={LedStatesString}")]
	internal class LutronKeypadState
	{
		//////////////////////////////////////////////////////////////////
		// Private State
		//

		/// <summary>
		/// Static (constant) information about this Lutron keypad.
		/// </summary>
		LutronKeypad m_staticData;

		//////////////////////////////////////////////////////////////////
		// Public Properties
		//

		/// <summary>
		/// The object that contains this object.
		/// </summary>
		internal LutronSimulator Owner;

		/// <summary>
		/// Buttons on this keypad, indexed by the 1-based button number.
		/// <c>Buttons[i]</c> is null for any unused button position, i.e.
		/// for any button number <c>i</c> for which there is no
		/// corresponding button.  <c>Buttons[0]</c> is always unused.
		/// This keypad has <r>ButtonCount</r> button positions, but
		/// <r>Buttons</r> has <c>ButtonCount + 1</c> entries because of
		/// button position 0 which doesn't count as a real Lutron button
		/// position.
		/// </summary>
		internal LutronButtonState[] Buttons;

		/// <summary>
		/// Indicates that one or more LEDs on this keypad changed since the
		/// last call to <r>ResetPendingLedNotifications</r>.
		/// </summary>
		internal bool LedsChanged;

		/// <summary>
		/// The number of button positions on the keypad.
		/// See <r>Buttons</r>.
		/// </summary>
		internal int ButtonCount
		{
			get
			{
				return Buttons.Length - 1;
			}
		}

		/// <summary>
		/// Keypad address, in the form "n.n.n", e.g. "1.4.7".  This
		/// address is "normalized": no leading zeroes, no ":" characters,
		/// no surrounding brackets, etc.
		/// </summary>
		internal string Address
		{
			get
			{
				return m_staticData.Address;
			}
		}

		/// <summary>
		/// Name of the control station, e.g. "Living Room Wall By Deck".
		/// </summary>
		internal string Name
		{
			get
			{
				return m_staticData.Name;
			}
		}

		/// <summary>
		/// Gets an enumeration of the indices of each button position,
		/// from 1 to <r>ButtonCount</r>, inclusive, including unused
		/// button positions.
		/// </summary>
		///
		internal IEnumerable<int> ButtonPositions
		{
			get
			{
				for (int buttonId = 1; buttonId <= ButtonCount; buttonId++)
				{
					yield return buttonId;
				}
			}
		}

		/// <summary>
		/// Gets a string of <r>ButtonCount</r> digits, each 1 if the LED of
		/// the button at that position on the keypad is on, or 0 if that
		/// LED is off (or if there is no button at that position).
		/// </summary>
		internal string LedStatesString
		{
			get
			{
                if (ButtonCount < 0)
                    return String.Empty;
				char[] states = new char[ButtonCount];
				foreach (int buttonId in ButtonPositions)
				{
					LutronButtonState button = Buttons[buttonId];
					states[buttonId - 1] =
						(((button != null) && button.LedOn) ? '1' : '0');
				}
				return new string(states);
			}
		}

		//////////////////////////////////////////////////////////////////
		// Public Methods
		//

		internal LutronKeypadState(LutronSimulator owner,
			LutronKeypad staticData)
		{
			Owner = owner;
			m_staticData = staticData;
			Buttons = new LutronButtonState[staticData.Buttons.Length];
			foreach (int buttonId in Util.IndicesOf(staticData.Buttons))
			{
				Buttons[buttonId] = new LutronButtonState(
					this, buttonId, staticData.Buttons[buttonId]);
			}
		}

		/// <summary>
		/// Finds the <r>LutronButtonState</r> object corresponding to a
		/// button number.
		/// </summary>
		///
		/// <param name="buttonId">The 1-based button number.</param>
		///
		/// <remarks>
		/// On error, a <r>LutronInvalidCommandException</r> is thrown.
		/// </remarks>
		///
		internal LutronButtonState NumberToButton(int buttonId)
		{
			if ((buttonId < 1) || (buttonId > ButtonCount) ||
					(Buttons[buttonId] == null))
				throw new LutronSimulator.LutronInvalidCommandException();
			else
				return Buttons[buttonId];
		}

		//////////////////////////////////////////////////////////////////
		// Overrides
		//

		public override string ToString()
		{
			// this override makes display of e.g.
			// <LutronSimulator.m_addressToKeypad> in the debugger more useful,
			// since KeyValuePair values are displayed using Key.ToString()
			// and Value.ToString()
			return Name;
		}
	}

	/// <summary>
	/// Represents a Lutron keypad button, or something (e.g. a CCI input)
	/// that behaves like a button.  Includes static information about the
	/// button, as well as its current simulated state.
	/// </summary>
	[DebuggerDisplay("{Keypad} {Address} LED={LedOnOff}")]
	public class LutronButtonState
	{
		//////////////////////////////////////////////////////////////////
		// Private State
		//

		/// <summary>
		/// Static (constant) information about this Lutron button.
		/// </summary>
		LutronButton m_staticData;

		//////////////////////////////////////////////////////////////////
		// Public Properties
		//

		/// <summary>
		/// The keypad that contains this button.
		/// </summary>
		internal LutronKeypadState Keypad;

		/// <summary>
		/// The 1-based button number of this button on the keypad.
		/// </summary>
		internal int ButtonId;

		/// <summary>
		/// Specifies if the button's LED is on.
		/// </summary>
		internal bool LedOn = false;

		/// <summary>
		/// The sequence (e.g. blinds up) that's currently executing, or
		/// null if none.
		/// </summary>
		internal LutronSequence ExecutingSequence;

		/// <summary>
		/// Gets a label, "ON" or "OFF", indicating if the LED is on or off.
		/// </summary>
		internal string LedOnOff
		{
			get
			{
				return LutronSimulator.OnOff(LedOn);
			}
		}

		/// <summary>
		/// When calculating which LEDs should be on or off, first all
		/// <r>LedPoints</r> are set to false.  Then, each item in each
		/// <s>LutronLoadState.LedContributions</s> of each <r>LutronLoadState</r> is
		/// examined: if the level of that load equals the level specified
		/// in the item, then the button specified in item gets one more
		/// "LED point" added to <r>LedPoints</r>.  When an LED reaches its
		/// "full" number of points, the LED is turned on (or off, if it has
		/// <r>LedReverse</r> specified).  <r>LedPoints</r> is not used for
		/// sequenced buttons (<r>IsSequenced</r> equal to <n>true</n>): for
		/// those, the LED is on while sequencing is happening.
		/// </summary>
		internal int LedPoints;

		/// <summary>
		/// The value of <r>LedPoints</r> at which the LED is turned on (or
		/// off, if it has <r>LedReverse</r> specified).
		/// <r>FullLedPoints</r> is not used for sequenced buttons
		/// (<r>IsSequenced</r> equal to <n>true</n>): for those, the LED
		/// is on while sequencing is happening.
		/// </summary>
		internal int FullLedPoints;

		/// <summary>
		/// Gets the address of this button, e.g. "1.2.3,4".
		/// </summary>
		internal string Address
		{
			get
			{
				return String.Format("{0},{1}", Keypad.Address, ButtonId);
			}
		}

		/// <summary>
		/// Returns true if this button exists on the keypad, false if not.
		/// </summary>
		internal bool Exists
		{
			get
			{
				return (m_staticData != null);
			}
		}

		/// <summary>
		/// Indicates that pressing the button initiates a timed sequence of
		/// actions, with each action consisting of one or more load
		/// assignments (e.g. turn a light on) and a delay (or "dwell")
		/// between each action.  If false, pressing the button results in
		/// a single action (consisting of one or more load assignments).
		/// </summary>
		internal bool IsSequenced
		{
			get
			{
				return m_staticData.IsSequenced;

			}
		}

		/// <summary>
		/// Indicates that the button behaves like a toggle (press to turn
		/// on scene, press again to turn off).
		/// </summary>
		internal bool Toggle
		{
			get
			{
				return m_staticData.Toggle;

			}
		}

		/// <summary>
		/// Indicates that the button LED has reverse logic, i.e. LED is on
		/// when scene lights are off.
		/// </summary>
		internal bool LedReverse
		{
			get
			{
				return m_staticData.LedReverse;

			}
		}

		/// <summary>
		/// Indicates that the last item in <r>Actions</r> is a "terminate"
		/// action.
		/// </summary>
		internal bool HasTerminateAction
		{
			get
			{
				return m_staticData.HasTerminateAction;

			}
		}

		/// <summary>
		/// The actions to execute when this button is pressed.  The last
		/// item in the array is the "terminate" action, if there is a
		/// terminate action (i.e. if <r>HasTerminateAction</r> is true).
		/// </summary>
		internal LutronAction[] Actions
		{
			get
			{
				return m_staticData.Actions;
			}
		}

		/// <summary>
		/// Gets an enumeration of each <r>LutronAssignment</r> associated
		/// with this button.
		/// </summary>
		///
		internal IEnumerable<LutronAssignment> Assignments
		{
			get
			{
				foreach (LutronAction action in Actions)
				{
					foreach (LutronAssignment assignment in
							action.Assignments)
						yield return assignment;
				}
			}
		}

#if DEBUG
		/// <summary>
		/// Gets a string that describes the actions performed when this
		/// button is pressed, for debugging purposes.
		/// </summary>
		internal string ActionsDescription
		{
			get
			{
				StringBuilder log = new StringBuilder(1000);
				foreach (int iAction in Util.IndicesOf(Actions))
				{
					LutronAction action = Actions[iAction];
					log.AppendFormat("action {0}:\n", iAction);
					if (HasTerminateAction &&
							(iAction == Actions.Length - 1))
						log.Append("terminate:\n");
					foreach (LutronAssignment assignment in
						action.Assignments)
					{
						log.AppendFormat(
							"  load {0} ({1}) <- {2} over {3}\n",
							assignment.LoadAddress,
							Keypad.Owner.AddressToLoad(
								assignment.LoadAddress),
							assignment.AssignValue, assignment.AssignFade);
					}
					if ((action.Dwell != "00:00") &&
						(iAction != Actions.Length - 1))
						log.AppendFormat("dwell {0}\n", action.Dwell);
				}
				return log.ToString();
			}
		}
#endif

		///////////////////////////////////////////////////////////////////
		// Public Methods
		//

		internal LutronButtonState(LutronKeypadState keypad, int buttonId,
			LutronButton staticData)
		{
			Keypad = keypad;
			ButtonId = buttonId;
			m_staticData = staticData;
		}

		//////////////////////////////////////////////////////////////////
		// Overrides
		//

		public override string ToString()
		{
			return Address;
		}
	}

	/// <summary>
	/// Represents a Lutron load i.e. an RPM (remote power module), Vareo
	/// (remotely controllable wall switch/dimmer), or CCO (relay).
	/// Includes static information about the load, as well as its current
	/// state.
	/// </summary>
	///
	[DebuggerDisplay("{Address}: {Name}: Level={Level}%")]
	internal class LutronLoadState
	{
		//////////////////////////////////////////////////////////////////
		// Private State
		//

		/// <summary>
		/// The object that contains this object.
		/// </summary>
		internal LutronSimulator Owner;

		/// <summary>
		/// Static (constant) information about this Lutron load.
		/// </summary>
		LutronLoad m_staticData;

		//////////////////////////////////////////////////////////////////
		// Public Properties
		//

		/// <summary>
		/// The level of the load, measured as a percentage value: 0 means
		/// fully off, 100 means fully on.
		/// </summary>
		internal int Level = 0;

		/// <summary>
		/// Gets the address of this Lutron load, in the form "n.n.n.n.n"
		/// for RPMs and Vareos and "n.n.n,n" for relays.
		/// </summary>
		internal string Address
		{
			get
			{
				return m_staticData.Address;
			}
		}

		/// <summary>
		/// Gets the name of the load, e.g. "Dining Room Ceiling".
		/// </summary>
		internal string Name
		{
			get
			{
				return m_staticData.Name;
			}
		}

		/// <summary>
		/// For each item in <r>LedContributions</r>, when this load is set
		/// at the level specified in the item, one "LED point" is
		/// contributed to the LED specified in the item.  When an LED
		/// reaches its "full" number of LED points, the LED is turned on
		/// (or off, if it has <r>LutronButton.LedReverse</r> specified).
		/// </summary>
		internal List<LedContribution> LedContributions =
			new List<LedContribution>();

		//////////////////////////////////////////////////////////////////
		// Public Methods
		//

		internal LutronLoadState(LutronSimulator owner, LutronLoad staticData)
		{
			Owner = owner;
			m_staticData = staticData;
		}

		//////////////////////////////////////////////////////////////////
		// Overrides
		//

		public override string ToString()
		{
			// this override makes display of e.g.
			// <LutronSimulator.m_addressToLoad> in the debugger more useful,
			// since KeyValuePair values are displayed using Key.ToString()
			// and Value.ToString()
			return Name;
		}
	}

	/// <summary>
	/// Specifies that when the load (<r>LutronLoadState</r>) that contains
	/// this object is set at a specified level (<r>Level</r> field),
	/// one "LED point" is contributed to a given LED (<r>Button</r> field).
	/// When the LED reaches its "full" number of LED points, the LED is
	/// turned on (or off, if it has <r>LutronButton.LedReverse</r> specified).
	/// </summary>
	///
	[DebuggerDisplay("LED {Button.Address}, level {Level}")]
	internal class LedContribution
	{
		/// <summary>
		/// The load level (percent from 0 to 100) which, when reached,
		/// causes one "LED point" to be contributed to the LED of the
		/// button specified by <r>Button</r>.
		/// </summary>
		internal int Level;

		/// <summary>
		/// The keypad button that's conditionally contributed to.
		/// </summary>
		internal LutronButtonState Button;
	}

	/// <summary>
	/// A sequence of <r>LutronSequenceStep</r> objects that each represent
	/// a step in a sequence associated with a button.
	/// </summary>
	///
	[DebuggerDisplay("Sequence for button {Button.Address} ({Button.Keypad.Name}), {Steps.Count} steps")]
	internal class LutronSequence
	{
		/// <summary>
		/// The object that contains this object.
		/// </summary>
		internal LutronSimulator Owner;

		/// <summary>
		/// The button associated with this sequence.
		/// </summary>
		internal LutronButtonState Button;

		/// <summary>
		/// When this sequence began, relative to the clock provided by
		/// <r cref="WorkQueue.Now">Owner.m_workQueue.Now</r>.
		/// </summary>
		internal When StartTime;

		/// <summary>
		/// The steps to perform in this sequence.  Each step groups the
		/// <r>LutronAssignment</r> objects that all need to start executing
		/// simultaneously.
		/// </summary>
		///
		/// <remarks>
		/// If <r>LutronButton.IsSequenced</r> is true, then the last element of
		/// <r>Steps</r> is the element to execute as the "termination step"
		/// if the user presses the button again to stop the sequence.
		/// </remarks>
		///
		internal List<LutronSequenceStep> Steps;

		/// <summary>
		/// The index into <r>Steps</r> of the next step to index.
		/// </summary>
		internal int NextStepIndex;

		/// <summary>
		/// The time at which the next step (see <r>NextStepIndex</r>)
		/// should begin executing, based on the clock provided by
		/// <r cref="WorkQueue.Now">Owner.m_workQueue.Now</r>.
		/// </summary>
		internal When NextStepTime;

		/// <summary>
		/// Gets or sets whether all steps of this sequence have completed.
		/// </summary>
		internal bool AtEnd
		{
			get
			{
				return (NextStepIndex >= Steps.Count);
			}
			set
			{
				NextStepIndex = Steps.Count;
			}
		}

#if DEBUG
		/// <summary>
		/// Gets a string that describes the actions performed when this
		/// button is pressed, for debugging purposes.
		/// </summary>
		internal string Description
		{
			get
			{
				StringBuilder log = new StringBuilder(1000);
				int iStep = 0;
				foreach (LutronSequenceStep step in Steps)
				{
					log.AppendFormat("step {0}:\n", iStep);
					if (Button.HasTerminateAction &&
						(iStep == Steps.Count - 1))
						log.Append("terminate:\n");
					foreach (LutronAssignment assignment in
						step.Assignments)
					{
						log.AppendFormat(
							"  load {0} ({1}) <- {2} over {3}\n",
							assignment.LoadAddress,
							Owner.AddressToLoad(assignment.LoadAddress),
							assignment.AssignValue, assignment.AssignFade);
					}
					if (iStep < Steps.Count - 1)
						log.AppendFormat("dwell {0}\n",
							When.FormatTime(step.DwellTime));
					iStep++;
				}
				return log.ToString();
			}
		}
#endif

		/// <summary>
		/// Executes the next step in this sequence.
		/// </summary>
		///
		/// <summary>
		/// Returns the step that was executed, or null if there are no
		/// further steps to execute.
		/// </summary>
		///
		internal LutronSequenceStep ExecuteNextStep()
		{
			// do nothing if there are no more steps to execute
			if (AtEnd)
				return null;

			// set <step> to the next step in <sequence>
			LutronSequenceStep step = Steps[NextStepIndex];

			// execute each assignment in <step>
			foreach (LutronAssignment assignment in step.Assignments)
				Owner.ExecuteAssignment(assignment);

			// advance to the next step in <sequence>
			NextStepIndex++;

			// set <NextStepTime> to be the time that the next step should
			// begin executing
			NextStepTime += step.DwellTime;

			// return the step that was executed
			return step;
		}

		/// <summary>
		/// Called when the next step of the sequence needs to be
		/// executed.
		/// </summary>
		///
		public void TimerCallback(object key)
		{
			lock (Owner.m_lock)
			{
				// execute the next step of the sequence
				ExecuteNextStep();

				if (AtEnd)
				{
					// we're at the end of the sequence of steps...

					// turn off the button's LED
					Owner.SetLedState(Button, false);

					// no sequence executing on <button> now
					Button.ExecutingSequence = null;
				}
				else
				{
					// queue the next step in the sequence
                    Owner.m_workQueue.Add(
                        new WorkItemWithKey<LutronSequence>(NextStepTime, this, delegate
                        {
                            TimerCallback(this);
                        }));
				}

				// send "KLS" notifications (if any) to the client, caused
				// by changes to LED states that occurred above
				Owner.UpdateLedsBasedOnLoads();
				Owner.SendLedNotifications();
			}
		}
	}

	/// <summary>
	/// One step in a <r>LutronSequence</r>.
	/// </summary>
	///
	[DebuggerDisplay("{Assignments.Length} assignments, {When.FormatTime(DwellTime)} dwell")]
	internal class LutronSequenceStep
	{
		/// <summary>
		/// All the assignments that should start executing at the same
		/// time.
		/// </summary>
		internal LutronAssignment[] Assignments; 

		/// <summary>
		/// The number of milliseconds to wait after all <r>Assignments</r>
		/// have executed, before executing the next
		/// <r>LutronSequenceStep</r> in the containing
		/// <r>LutronSequence</r>.
		/// </summary>
		internal long DwellTime;
	}

    /// <summary>
    /// The lock held by <c>LutronSimulator</c>.  This class has no real functionality -- it
    /// exists only to simplify debugging of lock objects.
    /// </summary>
    [DebuggerDisplay("{DebuggerDisplay}")]
    class LutronSimulatorLock
    {
#if DEBUG
        string DebuggerDisplay
        {
            get
            {
                return Util.GetLockDebuggerDisplay(this);
            }
        }
#endif
    }
}

}

